﻿// ****************************************************************************
// <copyright file="ObservableObject.cs" company="GalaSoft Laurent Bugnion">
// Copyright © GalaSoft Laurent Bugnion 2011-2013
// </copyright>
// ****************************************************************************
// <author>Laurent Bugnion</author>
// <email>laurent@galasoft.ch</email>
// <date>10.4.2011</date>
// <project>GalaSoft.MvvmLight.Messaging</project>
// <web>http://www.galasoft.ch</web>
// <license>
// See license.txt in this project or http://www.galasoft.ch/license_MIT.txt
// </license>
// ****************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

#if !SL3
using System.Linq;
using System.Linq.Expressions;
#endif

namespace TimersXP.Messaging
{
/// <summary>A base class for objects of which the properties must be observable.</summary>
    public class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
    {
        #region Events
        /// <summary>Occurs when a property value changes.</summary>
        public event PropertyChangedEventHandler PropertyChanged;
        /// <summary>Occurs before a property value changes.</summary>
        public event PropertyChangingEventHandler PropertyChanging;
        #endregion Events

        #region Event Handlers
        /// <summary>Provides access to the PropertyChanged event handler to derived classes.</summary>
        /// <value>The property changed handler.</value>
        protected PropertyChangedEventHandler PropertyChangedHandler { get { return PropertyChanged; } }
        /// <summary>Provides access to the PropertyChanging event handler to derived classes.</summary>
        /// <value>The property changing handler.</value>
        protected PropertyChangingEventHandler PropertyChangingHandler { get { return PropertyChanging; } }
        #endregion Event Handlers

        #region Public Functions
        /// <summary>
        /// Verifies that a property name exists in this ViewModel. This method can be called before the property is used, for instance before calling RaisePropertyChanged.
        /// It avoids errors when a property name is changed but some places are missed.
        /// </summary>
        /// <remarks>This method is only active in DEBUG mode.</remarks>
        /// <param name="propertyName">The name of the property that will be checked.</param>
        /// <exception cref="System.ArgumentException">Property not found</exception>
        [Conditional("DEBUG")]
        [DebuggerStepThrough]
        public void VerifyPropertyName(string propertyName)
        {
            var myType = GetType();
#if NETFX_CORE
            if (!string.IsNullOrEmpty(propertyName) && myType.GetTypeInfo().GetDeclaredProperty(propertyName) == null)
                throw new ArgumentException("Property not found", propertyName);
#else
            if (!string.IsNullOrEmpty(propertyName) && myType.GetProperty(propertyName) == null)
            {
#if !SILVERLIGHT
                var descriptor = this as ICustomTypeDescriptor;
                if (descriptor != null)
                {
                    if (descriptor.GetProperties()
                        .Cast<PropertyDescriptor>()
                        .Any(property => property.Name == propertyName))
                    {
                        return;
                    }   
                }
#endif
                throw new ArgumentException("Property not found", propertyName);
            }
#endif
        }
        #endregion Public Functions

        #region Protected Functions
        #region Virtual Functions
#if CMNATTR

        /// <summary>Raises the PropertyChanging event if needed.</summary>
        /// <remarks>If the propertyName parameter does not correspond to an existing property on the current class, an exception is thrown in DEBUG configuration only.</remarks>
        /// <param name="propertyName">(optional) The name of the property that changed.</param>
        [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "This cannot be an event")]
        protected virtual void RaisePropertyChanging( [CallerMemberName] string propertyName = null)
#else
        /// <summary>Raises the PropertyChanging event if needed.</summary>
        /// <remarks>If the propertyName parameter does not correspond to an existing property on the current class, an execption is thrown in DEBUG configuration only.</remarks>
        /// <param name="propertyName">The name of the property that changed.</param>
        [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "This cannot be an event")]
        protected virtual void RaisePropertyChanging(string propertyName)
#endif
        {
            VerifyPropertyName(propertyName);
            var handler = PropertyChanging;
            if (handler != null)
                handler(this, new PropertyChangingEventArgs(propertyName));
        }

#if CMNATTR
        /// <summary>Raises the PropertyChanged event if needed.</summary>
        /// <remarks>If the propertyName parameter does not correspond to an existing property on the current class, an exception is thrown in DEBUG configuration only.</remarks>
        /// <param name="propertyName">(optional) The name of the property that changed.</param>
        [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "This cannot be an event")]
        protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
#else
        /// <summary>Raises the PropertyChanged event if needed.</summary>
        /// <remarks>If the propertyName parameter does not correspond to an existing property on the current class, an exception is thrown in DEBUG configuration only.</remarks>
        /// <param name="propertyName">The name of the property that changed.</param>
        [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "This cannot be an event")]
        protected virtual void RaisePropertyChanged(string propertyName)
#endif
        {
            VerifyPropertyName(propertyName);
            var handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }

#if !SL3
        /// <summary>Raises the PropertyChanging event if needed.</summary>
        /// <typeparam name="T">The type of the property that changes.</typeparam>
        /// <param name="propertyExpression">An expression identifying the property that changes.</param>
        [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "This cannot be an event")]
        [SuppressMessage("Microsoft.Design", "CA1006:GenericMethodsShouldProvideTypeParameter", Justification = "This syntax is more convenient than other alternatives.")]
        protected virtual void RaisePropertyChanging<T>(Expression<Func<T>> propertyExpression)
        {
            var handler = PropertyChanging;
            if (handler != null)
            {
                var propertyName = GetPropertyName(propertyExpression);
                handler(this, new PropertyChangingEventArgs(propertyName));
            }
        }

        /// <summary>Raises the PropertyChanged event if needed.</summary>
        /// <typeparam name="T">The type of the property that changed.</typeparam>
        /// <param name="propertyExpression">An expression identifying the property that changed.</param>
        [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "This cannot be an event")]
        [SuppressMessage("Microsoft.Design", "CA1006:GenericMethodsShouldProvideTypeParameter", Justification = "This syntax is more convenient than other alternatives.")]
        protected virtual void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                var propertyName = GetPropertyName(propertyExpression);
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
#endif
        #endregion Virtual Functions
//Continued from above in the Virtual Functions region of code!
#if !SL3
        /// <summary>Extracts the name of a property from an expression.</summary>
        /// <typeparam name="T">The type of the property</typeparam>
        /// <param name="propertyExpression">An expression returning the property's name.</param>
        /// <returns>The name of the property returned by the expression.</returns>
        /// <exception cref="System.ArgumentNullException">If the expression is null.</exception>
        /// <exception cref="System.ArgumentException">
        /// Invalid argument;propertyExpression
        /// or
        /// Argument is not a property;propertyExpression
        /// <para>If the expression does not represent a property</para>
        /// </exception>
        [SuppressMessage("Microsoft.Design", "CA1011ConsiderPassingBaseTypesAsParameters", Justification = "This syntax is more convenient than the alternatives."),
        SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This synatx is more convenient than the alternatives.")]
        protected static string GetPropertyName<T>(Expression<Func<T>> propertyExpression)
        {
            if (propertyExpression == null)
                throw new ArgumentNullException("propertyExpression");

            var body = propertyExpression.Body as MemberExpression;
            if (body == null)
                throw new ArgumentException("Invalid argument", "propertyExpression");

            var property = body.Member as PropertyInfo;
            if (property == null)
                throw new ArgumentException("Argument is not a property", "propertyExpression");

            return property.Name;
        }

        /// <summary>Assigns a new value to the property. Then raises the PropertyChanged event if needed.</summary>
        /// <typeparam name="T">The type of the property that changed.</typeparam>
        /// <param name="propertyExpression">An expression identifying the property that changed.</param>
        /// <param name="field">The field storing the property's value.</param>
        /// <param name="newValue">The property's value after the change occurred.</param>
        /// <returns>True if the PropertyChanged event has been raised, false otherwise. The event is not raised if the old value is equal to the new value.</returns>
        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This syntax is more convenient than the alternative."),
        SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "1#", Justification = "This syntax is more convenient than the alternatives.")]
        protected bool Set<T>(Expression<Func<T>> propertyExpression, ref T field, T newValue)
        {
            if (EqualityComparer<T>.Default.Equals(field, newValue))
                return false;

            RaisePropertyChanging(propertyExpression);
            field = newValue;
            RaisePropertyChanging(propertyExpression);
            return true;
        }

        /// <summary>Assigns a new value to the property. Then raises the PropertyChanged event if needed.</summary>
        /// <typeparam name="T">The type of the property that changed.</typeparam>
        /// <param name="propertyName">The name of the property that changed.</param>
        /// <param name="field">The field storing the property's value.</param>
        /// <param name="newValue">The property's value after the change occured.</param>
        /// <returns>True if the PropertyChanged event has been raised, false otherwise. The event is not raised if the old value is equal to the new value.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "1#", Justification = "This syntax is more convenient than the alternatives.")]
        protected bool Set<T>(string propertyName, ref T field, T newValue)
        {
            if (EqualityComparer<T>.Default.Equals(field, newValue))
                return false;

            RaisePropertyChanging(propertyName);
            field = newValue;
            RaisePropertyChanged(propertyName);
            return true;
        }
#endif

#if CMNATTR
        /// <summary>Assigns a new value to the property. Then raises the PropertyChanged event if needed.</summary>
        /// <typeparam name="T">The type of the property that changed.</typeparam>
        /// <param name="field">The field storing the property's value.</param>
        /// <param name="newValue">The property's value after the change occurred.</param>
        /// <param name="propertyName">(optional) The name of the property that changed.</param>
        /// <returns>True if the PropertyChanged event has been raised, false otherwise. The event is not raised if the old value is equal to the new value.</returns>
        protected bool Set<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null) { return Set(propertyName, ref field, newValue); }
#endif
        #endregion Protected Functions
    }
}
